Skip to content

[中文版本](/posts/c教程/zh-CN/09-可移植与现代 C/086-Atomic Operations and Memory Models)

When multiple threads share data, you usually protect that data with locks likepthread_mutex_t. But sometimes, you need something faster, a way to perform an update that can’t be interrupted, even across threads. That’s where atomic operations come in.

This section introduces atomic operations in C and how the memory model ensures your program behaves predictably across cores.

Step 1. What Does “Atomic” Mean? ​

An atomic operation is one that happens all at once, it can’t be divided or interrupted.

Example idea: If two threads both runcounter++ at the same time:

  • Without atomicity → race condition.
  • With atomicity → one thread’s update completes fully before the other starts.

Atomic operations are essential in lock-free algorithms, concurrent queues, and reference counters.

Step 2. The Problem with counter++ ​

This line looks simple:

counter++;

But under the hood, it’s three separate steps:

  1. Loadcounter from memory.
  2. Increment it.
  3. Store it back.

Two threads doing this at once can lose updates:

Thread A: load(5)
Thread B: load(5)
Thread A: store(6)
Thread B: store(6)

Result: one increment lost, final value should be 7 but ends up 6.

Step 3. Using Atomic Types ​

C11 introduced<stdatomic.h>, a portable way to use atomic operations.

#include <stdatomic.h>
#include <stdio.h>
int main(void) {
    atomic_int counter = 0;
    atomic_fetch_add(&counter, 1);
    atomic_fetch_add(&counter, 1);
    printf("%d\n", counter); // 2
}

No locks. No race conditions. Theatomic_* functions guarantee the operations are atomic at the hardware level.

Step 4. Common Atomic Functions ​

FunctionDescription
atomic_loadRead atomically
atomic_storeWrite atomically
atomic_fetch_addAdd and return old value
atomic_fetch_subSubtract and return old value
atomic_exchangeReplace and return old value
atomic_compare_exchange_strongCompare-and-swap

Example:

atomic_compare_exchange_strong(&counter, &expected, desired);

Ifcounter == expected, replace it withdesired. Otherwise, updateexpected with the current value.

Step 5. Tiny Code: Atomic Counter with Threads ​

#include <stdio.h>
#include <pthread.h>
#include <stdatomic.h>
atomic_int counter = 0;
void* work(void* arg) {
    for (int i = 0; i < 100000; i++)
        atomic_fetch_add(&counter, 1);
    return NULL;
}
int main(void) {
    pthread_t t1, t2;
    pthread_create(&t1, NULL, work, NULL);
    pthread_create(&t2, NULL, work, NULL);
    pthread_join(t1, NULL);
    pthread_join(t2, NULL);
    printf("Counter = %d\n", counter);
}

Output is always200000, with no mutexes and no data races.

Step 6. Relaxed vs Sequential Consistency ​

Atomic operations can have different memory orders. By default, they’re sequentially consistent, the strongest and safest ordering.

Memory OrderMeaning
memory_order_seq_cstGlobal consistent order (default)
memory_order_relaxedOnly atomicity guaranteed
memory_order_acquirePrevent reordering before load
memory_order_releasePrevent reordering after store
memory_order_acq_relAcquire + Release combo

Example:

atomic_fetch_add_explicit(&counter, 1, memory_order_relaxed);

This is faster but weaker, use only when you understand your memory model.

Step 7. Memory Barriers and Visibility ​

Modern CPUs reorder reads/writes for performance. Atomics, fences, and locks control when updates become visible to other threads.

Example: Thread A writesready = 1. Thread B waits until it seesready == 1. If the compiler reorders memory operations, Thread B might not see the change.

Use:

atomic_thread_fence(memory_order_seq_cst);

to prevent reordering across the fence.

Step 8. Compare-and-Swap (CAS) ​

CAS is the backbone of lock-free data structures.

int expected = 0;
int desired = 1;
if (atomic_compare_exchange_strong(&counter, &expected, desired)) {
    printf("Swapped!\n");
}

It atomically checks ifcounter == expected and updates it, all in one instruction. This is used to build things like spinlocks, queues, and reference counters.

Step 9. Spinlocks with Atomics ​

A spinlock keeps checking until it can acquire the lock.

#include <stdatomic.h>
#include <unistd.h>
atomic_flag lock = ATOMIC_FLAG_INIT;
void lock_spin(void) {
    while (atomic_flag_test_and_set(&lock))
        ; // busy wait
}
void unlock_spin(void) {
    atomic_flag_clear(&lock);
}

This is efficient when the lock is held for a very short time. For longer waits, usepthread_mutex_t instead.

Step 10. Tiny Code: Atomic Reference Counter ​

#include <stdatomic.h>
#include <stdio.h>
typedef struct {
    atomic_int refcount;
} Object;
void retain(Object* obj) {
    atomic_fetch_add(&obj->refcount, 1);
}
void release(Object* obj) {
    if (atomic_fetch_sub(&obj->refcount, 1) == 1)
        printf("Object freed\n");
}
int main(void) {
    Object obj = { .refcount = 1 };
    retain(&obj);
    release(&obj);
    release(&obj);
}

Output:

Object freed

This is how many real-world systems (e.g. file handles, shared memory) track usage.

Why It Matters ​

Atomic operations are the building blocks of lock-free programming. They allow you to write high-performance concurrent code without blocking other threads. The C memory model gives you guarantees to reason about correctness even across multiple CPU cores.

Try It Yourself ​

  1. Replace a mutex counter with an atomic counter.
  2. Implement a spinlock usingatomic_flag.
  3. Useatomic_compare_exchange_strong to build a simple CAS loop.
  4. Test the difference betweenmemory_order_relaxed andseq_cst.
  5. Build a reference-counted structure using atomics.

Next, you’ll explore using C with other languages (FFI), how to make C libraries callable from Python, Rust, and Go.

本站总访问 — 次